package javagamecontroller.connection;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import util.input.android.events.AndroidVibration;
import javagamecontroller.activities.ConnectScreen;
import javagamecontroller.activities.SelectController;
import javagamecontroller.activities.controllers.AndroidVibrationThread;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.util.Log;


/**
 * This class is the main message handler. It will create the appropriate
 * connection and messaging threads and notify UI threads upon important
 * changes.
 * It also stores information sent by the server and delivers it to the android
 * app.
 * if necessary.
 * 
 * I choose to make this class a singleton which I feel is a good design choice.
 * An android controller should only control one game at a time. One could argue
 * that
 * a controller could control more than one game - but this doesn't happen in
 * real life, creates problems with bluetooth channels. By making the class a
 * singleton, the various
 * components and activities in the app can access information about the
 * connection, and send events that they generate.
 * 
 * @author Ben Schwab
 * 
 */
public final class BluetoothMessenger {

    private static BluetoothMessenger ourInstance = null;
    private State myState = State.None;
    private ConnectorThread myConnectThread;
    private MessengerThread myConnectedThread;
    private Handler myHandler;
    private boolean[] myControllerConfigs;
    private Map<String, String> myControllerDescriptions = new HashMap<String, String>();
    private SelectController mySubscribedScreen;
    private Handler mySubscribedScreenHandler;
    private Vibrator myVibrator;

    /**
     * 
     * The different connection states the messenger may be in.
     * 
     */
    public enum State {
        None, Listening, Connecting, Connected
    };

    private BluetoothMessenger () {
        // Exists only to defeat instantiation.
    }

    /**
     * 
     * @return An instance of the bluetooth messenger
     */
    public static BluetoothMessenger getInstance () {
        if (ourInstance == null) {
            ourInstance = new BluetoothMessenger();
        }
        return ourInstance;
    }

    public synchronized void connect (BluetoothDevice device, ConnectScreen connector,
                                      Handler uiHandle) {
        setState(State.Connecting);
        myHandler = uiHandle;

        // Cancel any thread attempting to make a connection
        if (myState == State.Connecting) {
            if (myConnectThread != null) {
                myConnectThread = null;
            }
        }

        // Cancel any thread currently running a connection
        if (myConnectedThread != null) {
            myConnectedThread.cancel();
            myConnectedThread = null;
        }

        // Start the thread to connect with the given device
        myConnectThread = new ConnectorThread(device, connector, this);
        myConnectThread.start();

    }

    /**
     * Set the current state of the chat connection
     * 
     * @param state An integer defining the current connection state
     */
    private synchronized void setState (State currentState) {
        myState = currentState;
    }

    /**
     * Return the current connection state.
     */
    public synchronized State getState () {
        return myState;
    }

    /**
     * Start the chat service. Specifically start AcceptThread to begin a
     * session in listening (server) mode. Called by the Activity onResume()
     */
    public synchronized void start () {

        // Cancel any thread attempting to make a connection
        if (myConnectThread != null) {
            myConnectThread.cancel();
            myConnectThread = null;
        }

        // Cancel any thread currently running a connection
        if (myConnectedThread != null) {
            myConnectedThread.cancel();
            myConnectedThread = null;
        }
        setState(State.Listening);
    }

    /**
     * Notify the messenger that the connection was lost.
     */
    public void connectionFailed () {
        setState(State.Listening);
        Bundle b = new Bundle();
        b.putInt(ConnectScreen.ACTION, ConnectScreen.CONNECT_FAILURE);
        sendUIMessage(b);
    }

    /**
     * Start the ConnectedThread to begin managing a Bluetooth connection
     * 
     * @param socket The BluetoothSocket on which the connection was made
     * @param device The BluetoothDevice that has been connected
     */
    public synchronized void startMessengerThread (BluetoothSocket socket, BluetoothDevice device) {

        // Cancel the thread that completed the connection
        if (myConnectThread != null) {
            myConnectThread = null;
        }

        // Cancel any thread currently running a connection
        if (myConnectedThread != null) {
            myConnectedThread.cancel();
            myConnectedThread = null;
        }

        // Start the thread to manage the connection and perform transmissions
        myConnectedThread = new MessengerThread(socket, this);
        myConnectedThread.start();
        Bundle b = new Bundle();
        b.putInt(ConnectScreen.ACTION, ConnectScreen.CONNECT_SUCCESS);
        sendUIMessage(b);
        setState(State.Connected);

    }

    /**
     * Call this method if the connection the server is lost.
     */
    public void connectionLost () {
        if (myConnectedThread != null) {
            myConnectedThread.cancel();
        }
        myConnectedThread = null;
        setState(State.Listening);
    }

    /**
     * Send a message to the connect screen.
     * 
     * @param messageInfo the bundle containing the message to send to the UI
     */
    public void sendUIMessage (Bundle messageInfo) {
        Message msg = myHandler.obtainMessage();
        msg.setData(messageInfo);
        myHandler.sendMessage(msg);
    }

    /**
     * Write to the ConnectedThread in an unsynchronized manner
     * 
     * @param out The bytes to write
     * @see ConnectedThread#write(byte[])
     */
    public void write (Serializable event) {

        MessengerThread r;
        // Synchronize a copy of the ConnectedThread
        synchronized (this) {
            if (myState != State.Connected) { return; }

            r = myConnectedThread;
        }
        // Create temporary object
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutput out = null;
        byte[] outBytes;
        try {
            out = new ObjectOutputStream(bos);
            out.writeObject(event);
            byte[] yourBytes = bos.toByteArray();
            outBytes = yourBytes;
            r.write(outBytes);
        }
        catch (IOException e) {

        }
        finally {
            try {
                out.close();
                bos.close();
            }
            catch (IOException e) {

            }
        }
    }

    /**
     * sets the controller configs for this connection - notifies all UI screens
     * that need to know about the config options.
     * 
     * @param controllerConfigs the controller config array
     */
    public void setControllerConfig (boolean[] controllerConfigs) {
        myControllerConfigs = controllerConfigs;
        if (mySubscribedScreen != null) {
            Message msg = new Message();
            Bundle b = new Bundle();
            b.putInt(SelectController.ACTION, SelectController.CONFIG_CHANGE);
            msg.setData(b);
            mySubscribedScreen.changeConfigSettings(myControllerConfigs);
            mySubscribedScreenHandler.sendMessage(msg);
        }
    }

    /**
     * 
     * @return the controllerConfig
     */
    public boolean[] getControllerConfigs () {
        return myControllerConfigs;
    }

    /**
     * Subscibe to updates about controller config changes
     * 
     * @param controllerScreen
     * @param handler
     */
    public void subscribeToControllerConfigChanges (SelectController controllerScreen,
                                                    Handler handler) {
        mySubscribedScreen = controllerScreen;
        mySubscribedScreenHandler = handler;

    }

    /**
     * subscribe to updates about controller description changes
     * 
     * @param descriptions
     */
    public void setControllerDescription (Map<String, String> descriptions) {
        for (String s : descriptions.keySet()) {
            myControllerDescriptions.put(s, descriptions.get(s));
        }
        if (mySubscribedScreen != null) {
            Message msg = new Message();
            Bundle b = new Bundle();
            b.putInt(SelectController.ACTION, SelectController.DESCRIPTION_CHANGE);
            msg.setData(b);
            mySubscribedScreen.changeControllerDescriptions(myControllerDescriptions);
            mySubscribedScreenHandler.sendMessage(msg);
        }
    }

    /**
     * 
     * @return the map of strings containing controller descriptons
     */
    public Map<String, String> getControllerDescriptions () {
        return myControllerDescriptions;
    }

    /**
     * If your app supports vibrator feedback you must set the vibrator
     */
    public void setVibrator (Vibrator v) {
        myVibrator = v;
    }

    /**
     * Preform a vibration event
     * 
     * @param vibrationEvent
     */
    public void performVibration (AndroidVibration vibrationEvent) {
        if (myVibrator == null) { return; }
        AndroidVibrationThread vibrate = new AndroidVibrationThread(vibrationEvent, myVibrator);
        vibrate.start();
    }

}
